﻿//-------------------------------------------------------------------------------------------------
// <copyright file="CustomPaintTextBox.cs" company="Anthony Adam">
// Copyright (c) Anthony Adam 2012. All rights reserved.
// </copyright>
//-------------------------------------------------------------------------------------------------
// ReSharper disable once CheckNamespace

using SuperStarRounds.DockingTasks;

namespace SuperStarRounds
{
  using System;
  using System.Collections;
  using System.Drawing;
  using System.Text.RegularExpressions;
  using System.Windows.Forms;
  using NHunspell;
  using Localisation;

  /// <summary>
  /// Utility Class for the spell checking text box and spell checking controls that are text box based.
  /// </summary>
  public class CustomPaintTextBox : NativeWindow
  {
    /// <summary>
    /// This references the owner text box control that will have the spell check bitmap overlay.
    /// </summary>
    private readonly TextBox parentTextBox;
    
    /// <summary>
    /// This bitmap is where we draw the wiggly lines so that when overlayed over the text box, the lines
    /// appear under the correct words to indicate spelling errors.
    /// </summary>
    private readonly Bitmap bitmap;
    
    /// <summary>
    /// Graphics object that is used to draw with.
    /// </summary>
    private readonly Graphics textBoxGraphics;
    
    /// <summary>
    /// Graphics buffer we draw into.
    /// </summary>
    private readonly Graphics bufferGraphics;

    /// <summary>
    /// Refers the spell checking engine
    /// </summary>
    private readonly Hunspell spellingChecker;
    
    /// <summary>
    /// Refers the thesaurus engine
    /// </summary>
    private readonly MyThes thesaurusLookUp;
    
    /// <summary>
    /// Ued to hold the correct spellings or alternate words to be offered when right clicking 
    /// over words in the parent control.
    /// </summary>
    private MatchCollection words;

    /// <summary>
    /// Character position of the start of the word right clicked on/spell checked.
    /// </summary>
    private int wordStart;
    
    /// <summary>
    /// Character position of the end of the word right clicked on/spell checked.
    /// </summary>
    private int wordEnd;
    
    /// <summary>
    /// Cartesian co-ordinate of the start of a wavy line.
    /// </summary>
    private Point start;
    
    /// <summary>
    /// Cartesian co-ordinate of the end of a wavy line
    /// </summary>
    private Point end;

    /// <summary>
    /// The word that was right clicked upon
    /// </summary>
    private string clickedWord;
    
    /// <summary>
    /// The pop up context menu that contains the spelling/thesaurus suggestions.
    /// </summary>
    private readonly ContextMenu spellSuggestions = new ContextMenu();

    /// <summary>
    /// Reference to the master form.
    /// </summary>
    private readonly BaseDockingTask myForm;

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomPaintTextBox"/> class
    /// </summary>
    /// <param name="thisForm">Reference to the owning form for the control</param>
    /// <param name="textBox">The text box control that is to be spell checkable</param>
    /// <param name="spellChecker">Reference to the spell checking engine</param>
    /// <param name="spellingThesaurus">Reference to the thesaurus engine</param>
    public CustomPaintTextBox(BaseDockingTask thisForm, TextBox textBox, Hunspell spellChecker, MyThes spellingThesaurus)
    {
      myForm = thisForm;
      parentTextBox = textBox;
      parentTextBox.MouseDown += MouseDownOnWord;
      parentTextBox.ContextMenu = spellSuggestions;
      spellingChecker = spellChecker;
      thesaurusLookUp = spellingThesaurus;

      bitmap = new Bitmap(textBox.Width, textBox.Height);
      bufferGraphics = Graphics.FromImage(bitmap);
      bufferGraphics.Clip = new Region(textBox.ClientRectangle);
      textBoxGraphics = Graphics.FromHwnd(textBox.Handle);

      // Start receiving messages (make sure you call ReleaseHandle on Dispose):
      AssignHandle(textBox.Handle);
    }

    /// <summary>
    /// Dispose event to allow easy tidy up when controls are created and disposed
    /// by code in the application.
    /// </summary>
    public void Dispose()
    {
      ReleaseHandle();
    }

    /// <summary>
    /// Intercept the Paint event for the TextBox at the OS level
    /// </summary>
    /// <param name="m">Windows internal message</param>
    protected override void WndProc(ref Message m)
    {
      switch (m.Msg)
      {
        case 15: // this is the WM_PAINT message
          // invalidate the TextBox so that it gets refreshed properly
          parentTextBox.Invalidate();
          
          // call the default win32 Paint method for the TextBox first
          base.WndProc(ref m);
          
          // now use our code to draw extra stuff over the TextBox
          CustomPaint(parentTextBox);
          break;
        default:
          base.WndProc(ref m);
          break;
      }
    }

    /// <summary>
    /// The words in view are checked before the text box is drawn, thus generating the correct
    /// bitmap overlay for the text box.
    /// </summary>
    /// <param name="textBox">The Textbox to be drawn</param>
    private void CustomPaint(TextBoxBase textBox)
    {
      // Private variable for sub stringing the word locations
      wordStart = 0;
      wordEnd = 0;
      var baseText = textBox.Text;

      // get the index of the first visible line in the TextBox
      var curPos = TextBoxApiHelper.GetFirstVisibleLine(textBox);
      TextBoxApiHelper.GetLineIndex(textBox, curPos);

      // clear the graphics buffer
      bufferGraphics.Clear(Color.Transparent);

      // * Here’s where the magic happens
      
      // start by ensuring we have some text
      if (!(string.IsNullOrEmpty(baseText) ||
            string.IsNullOrWhiteSpace(baseText)))
      {
        // get our word list...
        words = Regex.Matches(baseText, @"[\S]+");

        for (var i = 0; i < words.Count; i++)
        {
          var tempWord = words[i].ToString();
          wordEnd = wordStart + tempWord.Length - 1;

          while (wordEnd < baseText.Length &&
                 !baseText.Substring(wordStart, tempWord.Length).Equals(tempWord))
          {
            wordStart++;
            wordEnd++;
          }

          if (!baseText.Substring(wordStart, tempWord.Length).Equals(tempWord)) continue;

          // we have our word, so lets spell check it...
          // start by removing any BB code (bold and italics) in the word
          tempWord = tempWord.Replace("[b]", string.Empty).Replace("[/b]", string.Empty).Replace("[i]", string.Empty).Replace("[/i]", string.Empty);

          if (spellingChecker.Spell(tempWord)) continue;

          // Bad spelled word, so underline it wavy line
          start = TextBoxApiHelper.PosFromChar(textBox, wordStart);
          end = TextBoxApiHelper.PosFromChar(textBox, wordEnd + (wordEnd >= (textBox.Text.Length - 1) ? 0 : 1));
              
          // The position above now points to the top left corner of the character.
          // We need to account for the character height so the underlines go
          // to the right place.
          end.X += 1;
          start.Y += TextBoxApiHelper.GetBaselineOffsetAtCharIndex(textBox, wordStart);
          end.Y += TextBoxApiHelper.GetBaselineOffsetAtCharIndex(textBox, wordEnd);
              
          // Draw the wavy underline.
          DrawWave(start, end);
        }
      }

      // Now we just draw our internal buffer on top of the TextBox.
      // Everything should be at the right place.
      textBoxGraphics.DrawImageUnscaled(bitmap, 0, 0);
    }

    /// <summary>
    /// Draws the wavy line between the two provided positions.
    /// </summary>
    /// <param name="pointStart">Point of the start of the wavy line</param>
    /// <param name="pointEnd">Point of the end of the wavy line</param>
    private void DrawWave(Point pointStart, Point pointEnd)
    {
      var pen = Pens.Red;
      if ((pointEnd.X - pointStart.X) > 4)
      {
        var pl = new ArrayList();
        for (var i = pointStart.X; i <= (pointEnd.X - 2); i += 4)
        {
          pl.Add(new Point(i, pointStart.Y));
          pl.Add(new Point(i + 2, pointStart.Y + 2));
        }
        
        var p = (Point[])pl.ToArray(typeof(Point));
        bufferGraphics.DrawLines(pen, p);
      }
      else
        bufferGraphics.DrawLine(pen, pointStart, pointEnd);
    }

    /// <summary>
    /// Capture mouse down and if right click offer the spelling suggestions or the alternatives
    /// as appropriate.
    /// </summary>
    /// <param name="sender">Holds the object instantiating this event</param>
    /// <param name="e">Holds the event arguments</param>
    private void MouseDownOnWord(object sender, MouseEventArgs e)
    {
      if (e.Button != MouseButtons.Right) return;

      spellSuggestions.MenuItems.Clear();

      var clickedIndex = parentTextBox.GetCharIndexFromPosition(new Point(e.X, e.Y));
      var initialString = parentTextBox.Text.Substring(0, clickedIndex);
      var parentIndexOfSpace = parentTextBox.Text.IndexOf(" ", clickedIndex, StringComparison.Ordinal);
      var startIndex = parentIndexOfSpace != -1 ? parentIndexOfSpace : parentTextBox.Text.Length - 1;
      var lastIndex = initialString.LastIndexOf(" ", StringComparison.Ordinal);
      parentTextBox.SelectionStart = lastIndex + 1;
      parentTextBox.SelectionLength = startIndex - lastIndex - ((startIndex == (parentTextBox.Text.Length - 1)) ? 0 : 1);

      clickedWord = parentTextBox.SelectedText
        .Replace("[b]", string.Empty).Replace("[/b]", string.Empty)
        .Replace("[i]", string.Empty).Replace("[/i]", string.Empty);

      if (string.IsNullOrEmpty(clickedWord) || string.IsNullOrWhiteSpace(clickedWord)) return;

      // was it spelled badly
      if (!spellingChecker.Spell(clickedWord))
      {
        // get list of alternate words
        var alternateSpellings = spellingChecker.Suggest(clickedWord);
        var addToDic = new MenuItem
        {
          Name = "Add To Dictionary",
          Text = Localize.SpellingAddTo,
          Tag = "**Add To Dictionary**"
        };
        addToDic.Click += SuggestedWordClicked;
        spellSuggestions.MenuItems.Add(addToDic);

        var ignore = new MenuItem
        {
          Name = "Ignore This Session",
          Text = Localize.SpellingIgnore,
          Tag = "**Ignore**"
        };
        ignore.Click += SuggestedWordClicked;
        spellSuggestions.MenuItems.Add(ignore);

        if (alternateSpellings.Count <= 0) return;

        spellSuggestions.MenuItems.Add("-");

        for (var i = 0; i < alternateSpellings.Count; i++)
        {
          var item = new MenuItem
          {
            Name = "Suggestion " + i,
            Text = alternateSpellings[i],
            Tag = clickedWord
          };
          item.Click += SuggestedWordClicked;
          spellSuggestions.MenuItems.Add(item);
        }

        spellSuggestions.Show(parentTextBox, new Point(e.X, e.Y));
      }
      else
      {
        // if word is good, we'll do a thesaurus look up for alternatives instead
        // get list of alternate words
        var lookUp = thesaurusLookUp.Lookup(clickedWord, spellingChecker);

        if (lookUp == null) return;
        if (lookUp.IsGenerated) return;

        for (var i = 0; i < lookUp.Meanings.Count; i++)
        {
          var item = new MenuItem
          {
            Name = "Meaning " + i,
            Text = AdvisorStrings.CustomPaintTextBox_MouseDownOnWord_Meaning__ + lookUp.Meanings[i].Description
          };

          for (var j = 0; j < lookUp.Meanings[i].Synonyms.Count; j++)
          {
            var subitem = new MenuItem
            {
              Name = "Synonym " + j,
              Text = AdvisorStrings.CustomPaintTextBox_MouseDownOnWord_Synonym__ + lookUp.Meanings[i].Synonyms[j],
              Tag = clickedWord
            };
            subitem.Click += SynonymWordClicked;
            item.MenuItems.Add(subitem);
          }
                
          spellSuggestions.MenuItems.Add(item);
        }
              
        spellSuggestions.Show(parentTextBox, new Point(e.X, e.Y));
      }
    }

    /// <summary>
    /// When the context menu option is clicked, the word is set for ignoring for the
    /// session, or is added to the personal application dictionary, or is a replacement
    /// for the currently selected word.
    /// </summary>
    /// <param name="sender">Holds the object instantiating this event</param>
    /// <param name="e">Holds the event arguments</param>
    private void SuggestedWordClicked(object sender, EventArgs e)
    {
      var itemClicked = (MenuItem)sender;

      switch (itemClicked.Tag.ToString())
      {
        case "**Add To Dictionary**":
          // add to the in memory dictionary
          spellingChecker.Add(clickedWord);
          
          // and add and save to the personal dictionary
          myForm.AddToPersonalDictionary(clickedWord);
          break;

        case "**Ignore**":
          // add to the in memory dictionary only
          spellingChecker.Add(clickedWord);
          break;

        default:
          // else we are replacing the text
          parentTextBox.SelectedText = parentTextBox.SelectedText.Replace(itemClicked.Tag.ToString(), itemClicked.Text);
          break;
      }
    }

    /// <summary>
    /// When the context menu option is clicked, the word clicked replaces the
    /// currently selected word.
    /// </summary>
    /// <param name="sender">Holds the object instantiating this event</param>
    /// <param name="e">Holds the event arguments</param>
    private void SynonymWordClicked(object sender, EventArgs e)
    {
      var synonymClicked = (MenuItem)sender;

      parentTextBox.SelectedText = parentTextBox.SelectedText.Replace(synonymClicked.Tag.ToString(), synonymClicked.Text.Replace(AdvisorStrings.CustomPaintTextBox_MouseDownOnWord_Synonym__, string.Empty));
    }

    // TODO: Need a get working with the rtf text box to add spelling to Description.
  }
}